Skip to content

Redis入门_基础概念

待完善的一些内容

  • 常用的缓存读写策略
  • Redis 的常用数据类型
  • Zset 跳表
  • 分布式锁
  • Redis 线程模型
  • Redis 内存管理
  • 数据过期策略
  • 数据淘汰策略
  • Redis 性能优化
  • Redis 集群
  • Redis实现分布式锁如何合理的控制锁的有效时长
  • redis集群脑裂
  • 你们用过Redis的事务吗?事务的命令有哪些
  • Redis是单线程的,但是为什么还那么快?

1、Reids 的一些基础概念

一般来说,我们在学习使用 Redis 的时候,我们会先首先知道的是:Redis 存储的方式是作为一个键值存储的,其中键是字符串,值可以是字符串、列表、集合等复杂数据类型。

从最基础的数据类型的使用开始,我们才开始慢慢了解这款 Redis 内存存储系统;

由于 Redis 是基于内存存储,我们还会学习到 Redis 的持久化方案;了解到它的持久化特性之后,我们又开始对它的其他特性去掌握学习,包括事务(使用比较少)、发布/订阅模式、Lua 脚本、Key 过期等。

在使用的时候还会遇到很多问题,这些问题是怎么解决的呢,具体方案是什么,都是值得我们去了解和掌握的地方;

了解特性后,我们又会去学习:在高并发集群的情况下,Redis 的分布式集群又是怎么做的等。

Redis 是什么

基础介绍

  • 1、Redis(Remote Dictionary Server)是一个开源的、内存中的数据结构存储系统,能够快速读写,简单易用
  • 2、支持多种类型的数据结构,如字符串(Strings)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)以及哈希表(Hashes)等
  • 3、功能强大,支持事务、持久化、Lua脚本、集群等;使用场景丰富。

常见的一些基础面试题

Redis为什么这么快? 🏳️☃️

  • 基于内存操作
  • 数据结构优化
  • 单线程事件驱动模型
  • 专门为高性能而设计(专注于核心功能,设计简洁,功能强大)

这道题目的延申需要看一下(为什么这么快)

  • 1.数据存于内存 2.用了多路复用I/O 3.单线程

Redis 和 Memcached 的区别和共同点

区别点:实际回答的时候建议通过从Redis 的一些特性去回答,如果问到 Memcached 到时候可以从 数据类型、过期策略、集群、持久化方面去回答主要区别点。
  • 1、两者都是高性能的内存缓存系统,但在设计、使用、功能有一些异同
  • 2、同:基于内存、高性能读写、键值模型、用作缓存、都有过期策略
  • 3、不同
    • 持久化
    • 数据类型:Redis 的数据类型更加丰富,Memcached 支持简单的 ?
    • 分布式支持

待整理 to be contined...

区别

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  5. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
  7. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
  8. Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。

常用的缓存读写策略

缓存常用的三种读写策略:Cache Aside Pattern(旁路缓存模式)、Read/Write Through Pattern(读写穿透)、Write Behind Pattern(异步缓存写入)
  • 常用的缓存读写策略
    • Cache Aside Pattern(旁路缓存模式)
        • 从 cache 中读取数据,读取到就直接返回
        • cache 中读取不到的话,就从 db 中读取数据返回
        • 再把数据放到 cache 中。
        • 先更新 db
        • 然后直接删除 cache
    • Read/Write Through Pattern(读写穿透)
        • 从 cache 中读取数据,读取到就直接返回 。
        • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
        • 先查 cache,cache 中不存在,直接更新 db。
        • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
    • Write Behind Pattern(异步缓存写入)
        • **Read/Write Through 是同步更新 cache 和 db,
        • Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db

三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。

Cache Aside Pattern(旁路缓存模式)

适合读比较多的情况

写:

  • 先更新 db
  • 然后直接删除 cache

image.png

 :

  • 从 cache 中读取数据,读取到就直接返回
  • cache 中读取不到的话,就从 db 中读取数据返回
  • 再把数据放到 cache 中。

image.png

在写数据的过程中,可以先删除 cache ,后更新 db 么?

这种操作不能进行,会造成数据库和缓存数据不一致。

举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。

说明:

当有一个请求(我们称之为请求1)来更新某个数据项(称为数据A)时,你面临一个选择:是先更新数据库中的数据A,然后再删除缓存中的数据A,还是先删除缓存中的数据A,然后更新数据库?

如果选择后者(即先删除缓存,后更新数据库),会有以下风险:

  1. 请求1先删除了缓存中的数据A。
  2. 然后,在请求1更新数据库中的数据A之前,另一个请求(称之为请求2)来读取数据A。
  3. 请求2在缓存中找不到数据A(因为已经被删除),所以它从数据库中读取了旧的数据A。
  4. 此时,请求1更新了数据库中的数据A。
  5. 结果是,缓存中没有数据A的记录(已被删除),而数据库中有新的数据A。请求2持有的是过时的数据。

这种情况就导致了数据不一致:缓存失效了,但是请求2读取到了旧的数据。下次有请求来读取数据A时,它会再次从数据库中读取并缓存新的数据A,但在这之前,系统中存在数据不一致的状态。

通常,更好的做法是:

  1. 先更新数据库中的数据A。
  2. 然后删除缓存中的数据A。

这样,即使在数据库更新和缓存删除之间有读取请求,它也只会读到旧的数据,一旦缓存被删除,下一个读取请求将从数据库中获取最新的数据,并更新缓存,从而保持了一致性。

在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?

理论上仍然是会出现的,但概率非常小,缓存的写入速度比数据库的写入速度快很多。

特定情况:

  • 在数据库更新之后,但在缓存删除之前,如果另一个请求(称为请求2)来读取同一数据,它将从缓存中获取到旧的数据。因为此时缓存尚未被更新或删除。
  • 一旦缓存被删除,随后的请求将从数据库中读取最新的数据,并更新缓存。但在这个短暂的时期内,旧的缓存数据和新的数据库数据之间存在不一致。

Cache Aside Pattern 的缺陷以及解决方案

缺陷 1:首次请求数据一定不在 cache 的问题

解决办法:可以将热点数据可以提前放入 cache 中。

缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。

解决办法:

  • 数据库和缓存数据强一致场景:
    • 更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
  • 可以短暂地允许数据库和缓存数据不一致的场景:
    • 更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
Read/Write Through Pattern(读写穿透)

Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。

cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。

写(Write Through):

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

image.png

读(Read Through):

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

image.png

和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。

Read/Write Through Pattern(读写穿透)这种方式使用的不多,我们常用的分布式缓存Redis并没有提供 cache 将数据写入 db 的功能。

Write Behind Pattern(异步缓存写入)

Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。

不同点:

  • **Read/Write Through 是同步更新 cache 和 db,
  • Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db

数据一致性风险很大,不过Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

2、Redis 的常用数据类型

  • 5种基础数据类型,分别是:String、List、Set、Zset、Hash。
结构类型结构存储的值结构的读写能力
String字符串可以是字符串、整数或浮点数对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作;
List列表一个链表,链表上的每个节点都包含一个字符串对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素;
Set集合包含字符串的无序集合字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等
Hash散列包含键值对的无序散列表包含方法有添加、获取、删除单个元素
Zset有序集合和散列一样,用于存储键值对字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素
  • 三种特殊的数据类型 分别是 HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置)

Redis 的数据表示

image.png|300

主要讲述一下 Redis 的五种基础数据类型

相关的使用需要细讲一下 to be contined....

字符串(Strings)

  • 描述:字符串是Redis最基本的类型,可以存储任何形式的字符串(包括二进制数据),最大可以支持512MB。
  • 底层实现:简单动态字符串(Simple Dynamic Strings, SDS),它可以更有效地增长和缩小,避免了C字符串的缓冲区溢出问题。

集合(Sets)

  • 描述:集合是字符串的无序集合,且集合中的每个元素都是唯一的。
  • 底层实现:当元素数量较少且都是整数时,使用整数集合(intset)实现;否则,使用哈希表实现,以支持更大的集合和复杂元素。

Redis的集合是字符串类型的无序集合。它可以自动去重,非常适合存储不重复的数据集。

列表(Lists)

  • 描述:列表用于存储一个有序的字符串集合,类似于Java的LinkedList,支持在两端推入或弹出元素。
  • 底层实现:Redis列表使用双向链表(linked list)实现,也会根据实际使用场景在压缩列表(ziplist)和双向链表之间动态转换。小列表使用ziplist,可以节省空间,当列表元素较多或元素大小较大时,转换为linked list。

Redis列表是简单的字符串列表,按插入顺序排序。它可以用作队列,支持在列表的头部或尾部添加元素。

有序集合(Sorted Sets)

  • 描述:有序集合与集合类似,但每个元素都会关联一个双精度的分数,Redis会根据分数对元素进行排序。
  • 底层实现:跳表(skiplist)和哈希表的结合。跳表用于按分数排序和范围查询,而哈希表则用于快速访问元素。

每个元素都会关联一个双精度浮点数分数,Redis通过分数来为集合中的成员进行从小到大的排序。

为什么 ZSet 的底层结构使用跳表和哈希表的结合,细讲一下

哈希(Hashes)

  • 描述:哈希是键值对集合,适用于存储对象。
  • 底层实现:类似于列表的实现,小的哈希集合使用压缩列表(ziplist)来节省空间,而大的哈希集合使用哈希表。

适用于存储对象,类似于Java的HashMap,可以将多个键值对存储到一个Redis键里。

高级数据类型

位图(Bitmaps)、HyperLogLogs、地理空间索引(Geospatial Indexes)

这些高级数据类型支持更复杂的数据场景,如统计、地理位置服务等。

数据类型的一些扩展概念和使用

List

参考: https://blog.csdn.net/qq_61635026/article/details/132783244

3、Redis 应用(使用场景)

Redis 能做什么

  • 缓存系统
  • 分布式锁
  • 计数器/排行榜
  • 分布式 session(保存 token)
  • 消息队列、延时队列
  • 限流
  • 实时分析

缓存系统

  • 穿透、击穿、雪崩
  • 双写一致、持久化
  • 数据过期、淘汰策略

分布式锁

  • setnx redisson

分布式锁 🚩

见:Lottery微服务抽奖系统#2.17 设计滑动库存分布式锁处理活动秒杀 🚩

核心:setnx redisson

4 、Redis 持久化机制 🚩

官方文档地址:https://redis.io/topics/persistence 。

使用场景:使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

持久化方式

Redis 支持持久化,而且支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync策略),它们分别是:

appendfsync always    #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟调用fsync函数同步一次AOF文件
appendfsync no        #让操作系统决定何时进行同步,一般为30秒一次

AOF 持久化的fsync策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。

RDB

RDB持久化会在指定的时间间隔内生成内存中所有数据的快照并保存到硬盘上的一个RDB文件中。

这种持久化机制是通过创建数据的快照来实现的,它能够捕获存储在内存中的数据在某一特定时间点上的副本。

这种快照生成机制不仅允许Redis进行数据备份,以防数据丢失,还支持将这些快照文件复制到其他服务器,实现数据的迅速迁移或扩展,这在构建高可用的Redis主从复制结构中尤其有用。此外,RDB文件也可以被用于灾难恢复,当Redis服务器需要重启时,可以利用这些快照文件快速恢复到某一时间点的数据状态。

RDB方式的优势在于它提供了一个非常紧凑的数据文件,这使得数据恢复过程变得非常快速和高效。此外,由于RDB文件的生成是在服务器的子进程中完成的,主进程在此过程中不会进行任何磁盘I/O操作,这保证了Redis的高性能和数据的一致性。然而,使用RDB也需要注意数据安全性的问题,因为在两次快照之间的数据更改在发生故障时可能会丢失。

基本特点

  • 优点
    • 性能高:RDB持久化可以在不影响Redis主线程的情况下,在后台异步地进行数据快照保存,对性能影响较小。
    • 快速恢复:使用RDB进行数据恢复比AOF的方式更快,因为直接将快照文件读取到内存中即可。
  • 缺点
    • 数据丢失:如果Redis崩溃,你可能会丢失最后一次快照以来的所有数据。
    • 大数据集恢复时间较长:对于非常大的数据集,保存RDB快照的时间可能会比较长

快照持久化

快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。

RDB 创建快照时会阻塞主线程吗?

Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 同步保存操作,会阻塞 Redis 主线程;
  • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

这里说 Redis 主线程而不是主进程的主要是因为 Redis 启动之后主要是通过单线程的方式完成主要的工作(如果你想将其描述为 Redis 主进程,也没毛病 )。


AOF

与快照持久化相比,AOF 持久化的实时性更好。

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

开启 AOF 持久化操作

默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 appendonly 参数开启:

appendonly yes
AOF持久化的实现过程

AOF(Append Only File)持久化是Redis提供的一种数据持久化机制,通过记录所有对数据库进行修改的命令来实现数据的恢复。AOF持久化的实现过程主要分为以下五个步骤,每一步都是确保数据安全性和恢复能力的关键环节。

    1. 命令追加(Append)
    • 所有写命令(如SETDEL等)都会被追加到AOF缓冲区中。这一步是实时的,保证了所有对数据的修改操作都能被捕获。
    1. 文件写入(Write)
    • AOF缓冲区中的数据会周期性地写入到AOF文件中。这个过程涉及到write系统调用,它把数据从用户空间复制到内核空间的缓冲区中,但此时数据还没有被同步到磁盘,只是存储在了系统的内核缓冲区中。
    1. 文件同步(Fsync)
    • 根据配置的fsync策略(如每秒同步或每个写命令同步),Redis会定期调用fsync系统调用来强制将内核缓冲区中的数据同步到磁盘上。fsync操作会阻塞,直到所有的数据都安全写入磁盘后才返回,这一步确保了数据的持久性。
    1. 文件重写(Rewrite)
    • 随着时间的推移,AOF文件会因为不断追加新命令而变得越来越大。Redis提供了AOF重写机制,可以在不改变数据模型的前提下,通过重新生成一份优化的AOF文件来减小文件大小。这一过程通过重写并压缩AOF文件来实现,移除了文件中冗余的命令。
    1. 重启加载(Load)
    • 当Redis服务器重启时,它会重新加载AOF文件来恢复数据状态。这个过程通过逐一执行AOF文件中记录的命令来完成,从而恢复到最后一次同步时的数据状态。

系统调用的作用

  • write:将数据写入系统内核缓冲区后即返回,提高了写入效率,但存在数据丢失的风险。
  • fsync:强制从内核缓冲区同步数据到磁盘,确保数据的持久化。虽然fsync会增加I/O开销,但它保证了数据的安全性。

AOF持久化通过精确记录修改数据库的每个操作,并利用系统调用确保这些操作能被安全地存储到磁盘上,从而实现数据的持久化和恢复。

选择合适的fsync策略可以平衡性能和数据安全性,而AOF重写则是优化磁盘空间和提升性能的有效手段。


混合持久化

从Redis 4.0版本开始,引入了RDB和AOF的混合持久化模式,这种模式结合了两种持久化机制的优点:RDB的快速恢复能力和AOF的数据完整性保证。

这种方式兼顾了数据恢复的速度和数据安全性,但是也需要考虑其可能带来的额外磁盘和内存资源消耗。

工作原理

在混合持久化模式下,Redis在进行AOF重写时会同时将当前内存中的数据状态以RDB格式写入到AOF文件的头部,之后再将从启动或上一次AOF重写以来的所有写命令追加到文件末尾。

这样做的结果是一个包含完整数据快照(RDB格式)以及之后所有变更(AOF格式)的AOF文件。

这种方式的优点能够进行快速重启和保证了数据安全

  • 快速重启:使用RDB格式的数据快照作为AOF文件的一部分,可以在Redis重启时快速恢复整个数据集,因为加载RDB格式数据通常比执行AOF文件中的命令要快得多。
  • 数据安全:由于AOF日志包含了数据快照之后的所有写操作,这种混合模式保留了AOF的数据完整性特点,即使在Redis崩溃后也能保证数据不丢失。

使用配置

要启用混合持久化,需要在Redis配置文件中设置相应的配置项。下面是一些相关配置参数:

  • aof-use-rdb-preamble:设置为yes以启用混合持久化模式。这告诉Redis在执行AOF重写时,先以RDB格式写入当前内存快照,然后再追加写命令日志。

示例配置:

appendonly yes
aof-use-rdb-preamble yes

注意事项

  • 在混合模式下,AOF文件的大小可能会比纯AOF模式更大,因为它包含了完整的数据快照。因此,需要确保有足够的磁盘空间。
  • 虽然混合持久化提高了数据恢复的速度,但在高写入负载下,重写AOF文件可能会消耗大量的CPU和磁盘I/O资源。因此,应根据实际负载和资源情况合理配置AOF重写策略。
  • 混合持久化模式结合了RDB和AOF的优势,但也继承了它们的特点,包括RDB快照的间隔性和AOF日志的持续增长。因此,合理配置RDB快照和AOF重写的触发条件对于维护Redis的性能和数据安全至关重要。

5、Redis 线程模型 🚩💣

Redis是一个使用C语言编写的高性能键值对存储系统。它的线程模型特别之处在于其单线程架构,

不过,从Redis 4.0版本开始,引入了一些多线程特性来处理某些特定的任务,但核心数据读写操作仍然是单线程的。

单线程架构

在Redis的主要版本中,所有的数据读写操作都是由一个单独的线程来处理的。这意味着在任何给定时间点,只有一个操作能够访问数据集。这种设计有几个显著的优势:

  • 简化数据结构:由于不存在并发操作数据的情况,Redis可以使用简单高效的数据结构来存储数据,而不需要担心加锁和同步的问题。
  • 避免线程切换和锁的开销:线程切换和锁机制在多线程程序中会引入额外的开销。Redis通过使用单线程模型避免了这些开销,从而提高了性能。
  • 简化编程模型:单线程模型使得Redis的编程和调试变得更加简单直接,因为开发者不需要考虑复杂的并发问题。

I/O多路复用

Redis虽然在数据处理上采用单线程模型,但它使用I/O多路复用技术来高效地处理成千上万的并发连接。

I/O多路复用允许单个线程同时监视多个网络连接上的I/O事件,一旦某个连接上有数据可以读取或可以写入,相应的操作就会被执行。这使得Redis能够在不增加线程数量的情况下,实现高吞吐量和低延迟。

Redis 4.0以后的多线程

从Redis 4.0开始,引入了多线程来处理某些特定的任务,主要是网络I/O和键过期删除等操作,以减轻主线程的负担。但需要注意的是,这些多线程并不处理数据的读写操作,核心数据操作依然是单线程执行的。这种设计旨在保持Redis操作的简单性和高性能,同时提高资源的利用率。

Redis6.0 之后引入了多线程 ?

6、Redis 内存管理 🚩

阅读:

Redis 是一个高性能的键值对数据库,内存管理是其中一个非常重要的功能;

关于 Redis 内存管理的一些常见概念:

  • 内存分配器
    • Jemalloc:Redis 默认使用 Jemalloc 作为内存分配器,它对碎片化处理得比标准的 libc malloc 更加高效。
  • 过期键和内存淘汰
    • 过期策略
      • Redis 允许为键设置生存时间(TTL)。键到期后会被删除,释放内存。
    • 内存淘汰策略(在内存使用达到限制时,Redis 提供了多种淘汰策略)
      • noeviction:不淘汰任何数据,只是返回错误。
      • allkeys-lru:根据最近最少使用算法(LRU)淘汰数据。
      • volatile-lru:只从设置了过期时间的键中选择淘汰。
      • allkeys-random:随机淘汰。
      • volatile-random:从设置了过期时间的键中随机淘汰。
      • volatile-ttl:淘汰即将过期的键。
  • 内存配置
    • maxmemory 配置:可以在 Redis 配置文件中设置 maxmemory 选项来限制 Redis 使用的最大内存量。
    • maxmemory-policy:定义当内存达到限制时 Redis 应该执行的淘汰策略。
  • 内存使用监控
    • 内存使用指标:Redis 提供了丰富的指标来监控内存使用情况,例如 used_memoryused_memory_rssused_memory_peak
    • 内存优化命令:如 MEMORY PURGE(在某些版本中可用)可以用来尝试减少内存碎片。
  • 数据结构优化
  • 内存回收

过期时间

一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?

因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。

Redis 自带了给缓存数据设置过期时间的功能,比如:

127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

注意:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间。

过期时间还可以用于某些业务场景:比如手机短信验证码的有效期保持在 1 分钟,token 登录的有效期在 1 天内。

如何判断键是否过期

数据过期策略 🚩💣

数据淘汰策略 🚩💣

7、Redis 事务

什么是Redis 事务

不是很推荐使用Redis事务,跟我们常见的关系型数据库事务不同:

  • 1、Redis 事务是不支持回滚(roll back)操作的,Redis 事务不满足原子性。
  • 2、Redis 事务的持久性是没办法保证的

image.png

Lua 脚本

与事务相对的话,更推荐使用 Lua 脚本执行批量任务;

  • 一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。


Redis的Lua脚本功能是其强大特性之一,提供了在Redis服务器端执行脚本的能力。这允许执行一系列操作,这些操作作为一个整体原子地执行,而不是由客户端逐一发送命令然后由服务器逐一执行。

Lua是一种轻量级的编程语言,被嵌入到Redis中用于执行复杂的逻辑,这在多个命令需要作为单个原子操作执行时特别有用。

使用示例:

假设我们需要更新一个计数器,但只有在另一个键存在时才进行更新。不使用Lua脚本,我们可能需要先检查键是否存在,然后再更新计数器,这两步操作不能保证原子性。

使用Lua脚本,我们可以这样做:

if redis.call("EXISTS", KEYS[1]) == 1 then
    return redis.call("INCR", KEYS[2])
else
    return 0
end
  • 这个脚本接受两个键作为输入:KEYS[1]用于检查存在性,KEYS[2]是需要增加的计数器。如果KEYS[1]存在,脚本将增加KEYS[2]的值并返回新值;如果不存在,返回0。

执行脚本操作(使用EVAL命令执行Lua脚本):

EVAL <script> <numkeys> <key> [<key2> ...] [<arg> [<arg2> ...]]
  • <script>:Lua脚本文本。
  • <numkeys>:脚本中将要处理的键的数量。
  • <key>:脚本中用到的Redis键。
  • <arg>:传递给脚本的其他参数。

8、Redis 性能优化 🚩💣

2.1 使用批量操作减少网络传输

2.2 大量 key 集中过期问题

2.3 大 Key

2.4 热 Key

2.5 慢查询问题

为什么会有慢查询问题

Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令和一些时间复杂度可能在 O(N) 以上的命令;

由于这些命令执行时间复杂度较高,,有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。

由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。


O(n) 时间复杂度的命令:

  • KEYS *:会返回所有符合规则的 key。
  • HGETALL:会返回一个 Hash 中所有的键值对。
  • LRANGE:会返回 List 中指定范围内的元素。
  • SMEMBERS:返回 Set 中的所有元素。
  • SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
  • ……

时间复杂度可能在 O(N) 以上的命令:

  • ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
  • ……

开启慢查询日志

Redis慢查询日志是一种用来记录执行时间超过指定阈值的命令的功能。

查询日志通过两个配置参数控制:

  1. slowlog-log-slower-than:设置执行时间超过多少微秒的命令会被记录到慢查询日志中。默认值是10000微秒(即10毫秒)。如果设置为0,则记录所有的命令;如果设置为负数,则不记录任何命令。
  2. slowlog-max-len:设置慢查询日志的最大长度。当新的命令被记录而日志长度超过这个设置时,最旧的记录会被移除。

这些参数可以在Redis配置文件中设置,也可以通过CONFIG SET命令动态设置。

例如,动态设置记录执行时间超过20毫秒的命令,且慢查询日志最大长度为128:

CONFIG SET slowlog-log-slower-than 20000
CONFIG SET slowlog-max-len 128

查询慢查询日志

SLOWLOG GET 10

SLOWLOG GET 命令默认返回最近 10 条的的慢查询命令,也自己可以指定返回的慢查询命令的数量 SLOWLOG GET N

每条慢查询日志包含以下信息:

  • 唯一的慢查询ID
  • 命令执行的时间戳
  • 命令的执行时长(微秒)
  • 命令及其参数

重置慢查询日志

使用SLOWLOG RESET命令可以清空当前的慢查询日志:

SLOWLOG RESET

2.6 Redis 内存碎片

什么是内存碎片

内存碎片是不可用的空闲内存,

image.png|500

Redis 为什么会有内存碎片

常见的两个原因:

    1. Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
    1. 频繁修改 Redis 中的数据也会产生内存碎片。

Redis 的内存碎片化是指随着 Redis 运行时间的增长和数据的频繁更新,内存分配和释放可能导致可用内存被分割成小块,从而降低了内存的利用效率。内存碎片化在 Redis 中是一个重要的问题,因为它可以影响性能并导致内存使用量不必要地增加。

何时产生内存碎片

  1. 频繁的小对象分配和释放
    • 当应用频繁地创建和删除小对象时,如频繁地设置和删除键值对。
    • 这种操作会导致内存中出现很多小的、不连续的空间,造成碎片。
  2. 特定的数据类型操作
    • 某些数据类型的操作,如对列表、集合和哈希的频繁修改,也可能导致内存碎片化。
  3. 大量写入后的删除
    • 当大量数据被写入 Redis 并且之后又被删除时,尤其是这些数据的大小不一时。

如何处理和减少内存碎片化

  1. 监控内存碎片率
    • 使用 INFO memory 命令监控内存碎片率(mem_fragmentation_ratio)。如果碎片率高(例如大于 1.5),则可能需要采取措施。
  2. 定期重启
    • 定期重启 Redis 服务可以减少碎片化,但这可能会导致服务中断。
  3. 内存碎片整理
    • 在 Redis 4.0 及以上版本中,可以使用 MEMORY PURGE 命令(在某些版本中可用)来尝试减少内存碎片。
    • 使用 redis-cli --intrinsic-latency 命令检测内存碎片整理过程中的延迟。
  4. 使用更大的对象
    • 对于高频操作的数据,考虑将多个小对象合并成一个更大的对象。
  5. 调整内存分配器配置
    • 调整 Jemalloc 分配器的配置,可能有助于减少碎片化。
  6. 应用层优化
    • 在应用层优化数据访问模式,减少频繁的小对象写入和删除。
  7. 数据结构选择
    • 根据使用场景合理选择数据结构,有些数据结构可能更容易产生内存碎片。

实际业务场景下的应对策略

在实际业务中,需要根据具体的使用场景和数据模式来确定最佳的处理内存碎片化的策略。通常,需要定期监控 Redis 的内存碎片率,并在碎片化达到一定程度时采取相应措施。例如,可以在业务低峰期进行 Redis 的重启或内存碎片整理,以减少对业务的影响。同时,优化数据访问模式和选择合适的数据结构,也可以在长期内有效减少内存碎片化的发生。

9、Redis 生产问题 🚩

Redis 生产问题:缓存穿透、缓存击穿、缓存雪崩、数据一致性、Redis 阻塞

缓存穿透

缓存穿透是指当请求的数据既不在缓存中也不存在于数据库中时,导致请求直接到达数据库层的现象。

这通常发生在请求非法或不存在的数据时(这类数据不会被缓存),每次查询都会穿过缓存层直接查询数据库。

如果有大量此类查询,数据库可能会因此承受过大的压力

成因

  • 频繁查询不存在的数据(可能是由于错误的输入或恶意攻击)。

解决方案

  • 空对象缓存(缓存空结果)
    • 即使某个值在数据库中不存在,也可以在缓存中存储一个特殊的空对象或空值,并设置较短的过期时间。
    • 这样,相同的无效请求在这个过期时间内会直接得到缓存中的空结果,而不会再次查询数据库。
    • 缺点:可能会导致期间使用内存较高,缓存资源浪费
  • 布隆过滤器
    • 在查询之前使用布隆过滤器判断数据是否可能存在。布隆过滤器是一种空间效率高但可能有一定误判率的数据结构。
布隆过滤器

布隆过滤器可以参考的一个链接: https://mp.weixin.qq.com/s/DeqJfw51tPJvdwAkq3iPdg


布隆过滤器(Bloom Filter)

布隆过滤器是一种空间效率高的概率型数据结构,用于判断一个元素是否在一个集合中。它可能会有一定的误判率(即认为元素存在于集合中,但实际上并不存在),但不会漏报(即如果它说元素不在集合中,那么元素绝对不在集合中)。

通过布隆过滤器预先判断请求的数据是否可能存在,可以有效避免对不存在的数据进行查询。

组成

我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构

工作原理

布隆过滤器底层基于一个很长的二进制向量(位数组)和一系列随机映射函数(哈希函数)。

具体工作流程如下:

  1. 初始化:开始时,布隆过滤器是一个包含m位的数组,所有位都设置为0。
  2. 添加元素:当向过滤器添加一个元素时,该元素会被k个哈希函数分别计算哈希值,每个哈希函数将产生一个数组范围内的位置。然后将这些位置的位都设置为1。
  3. 查询元素:要检查一个元素是否在集合中,只需对该元素使用相同的k个哈希函数得到k个位置,如果这些位置的位都是1,则认为元素可能在集合中(存在误判)。如果任何一个位是0,则元素一定不在集合中。

具体使用示例;

Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。

首先需要确保你的Redis实例已经安装了RedisBloom模块。安装后,使用该模块提供的命令来创建和管理布隆过滤器。

#基本的RedisBloom布隆过滤器命令
#BF.ADD:向布隆过滤器添加元素: 向名为`myBloomFilter`的布隆过滤器中添加元素`item`。
BF.ADD myBloomFilter item

#BF.EXISTS:检查给定的元素是否可能存在于布隆过滤器中。
#如果命令返回1,表示`item`可能存在于过滤器中;如果返回0,则`item`绝对不在过滤器中。
#- BF.MADD:向布隆过滤器中添加多个元素。
#- BF.MEXISTS:检查多个元素是否可能存在于布隆过滤器中。
BF.EXISTS myBloomFilter item

布隆过滤器是什么时候加载的?

一般是预加载,布隆过滤器的加载通常在系统启动或在维护窗口进行,因此对实时性的影响可以控制。

一旦布隆过滤器建立,其查询效率是非常高的,对在线服务几乎没有影响。

一般是在系统初始化时,对于数据库中已有的数据,会将其键(或标识)添加到布隆过滤器中,从而实现过滤掉不存在的数据。

缓存击穿

缓存击穿是指缓存中某个热点key突然失效(过期),导致大量并发请求直接落到数据库上,造成数据库短时间内压力剧增的现象。

与缓存穿透不同,缓存击穿是针对原本就存在于缓存中的热点数据。

缓存击穿的问题

  • 热点数据在缓存中突然过期,而此时正有大量并发请求这些数据。
  • 核心在于热点key失效瞬间,所有对这个key的请求都会直接访问数据库,对数据库造成较大压力,甚至可能导致数据库瘫痪。

解决方案

  • 设置热点数据永不过期
    • 对于一些热点数据,可以设置其缓存永不过期,或者设置一个非常长的过期时间,从而避免缓存击穿问题。
    • 但这种方法需要定期手动或通过程序更新缓存,以保证数据的新鲜度
  • 互斥锁
    • 在缓存失效的瞬间,使用互斥锁或分布式锁,确保只有一个请求去数据库查询数据并重新缓存。
    • 其他请求等待缓存加载完成后再访问缓存
  • 提前预热
    • 在缓存即将过期前,后台异步程序提前更新缓存中的数据,这样可以确保热点数据在缓存中始终是可用的
  • 二级缓存策略
    • 为热点数据设置两级缓存,第一级缓存正常设置过期时间;第二级缓存设置较长的过期时间。当第一级缓存失效时,请求可以访问第二级缓存,同时异步更新第一级缓存和第二级缓存数据。

互斥锁的使用

当缓存失效时,不是所有请求都去数据库加载数据,而是使用某种互斥机制(如分布式锁)保证只有一个请求去数据库查询数据并加载到缓存中,其他请求等待缓存加载完成后再访问缓存。这种方式可以有效减少对数据库的访问压力。

示例伪代码:

if (cache.get(key) == null) {
    if (lock.tryLock()) {
        // 只有获得锁的请求去数据库查询并更新缓存
        data = database.find(key);
        cache.set(key, data);
        lock.unlock();
    } else {
        // 其他请求等待一段时间后重试
        Thread.sleep(100);
        return cache.get(key);
    }
}

使用互斥锁防止缓存击穿的具体步骤:

  • 1、检查缓存
  • 2、如果缓存不存在,进入下一步
  • 3、在访问数据库之前,尝试对这个特定的数据请求获取一个互斥锁。
    • 如果获取锁成功,进入下一步;这意味着当前请求被选中为去数据库中查询数据并更新缓存的请求。
    • 如果获取锁失败,当前请求进入等待状态,直到锁被释放。然后回到步骤1重新检查缓存(因为此时缓存可能已经被更新)。
  • 4、查询数据库
  • 5、更新缓存
    • 将查询到的数据更新到缓存中,并设置一个合理的过期时间。这一步很关键,它保证了后续的请求可以直接从缓存中获取数据。
  • 6、释放锁
    • 完成数据库查询和缓存更新后,释放步骤2中获取的锁,允许其他等待的请求继续执行

提前预热

提前预热的操作其实并不止自动延长过期时间;还包括静态预热、自动预热(手动/自动将热点数据提前放入缓存);

自动延迟的实现示例:

# 检查key的剩余过期时间
TTL hot_key

# 如果返回的剩余时间少于60秒,则延长过期时间
EXPIRE hot_key 300

二级缓存策略

二级缓存结构设计通常涉及在现有的缓存系统(第一级缓存)之外,额外设置一个缓存层(第二级缓存),这个额外的缓存层可以是同一种缓存技术,也可以是不同的缓存技术,具体取决于需求和环境。

缓存技术选择

  • 第一级缓存:通常位于应用服务器内部,如内存中的对象缓存(例如,Java中的HashMap或Guava Cache)。第一级缓存的主要目的是提供高速访问,减少网络延迟。
  • 第二级缓存:通常位于应用服务器外部,可以是分布式缓存系统(例如Redis或Memcached)。第二级缓存用于存储更多数据,提供相对较长的数据保存时间,保证数据的持久性和稳定性。

两级缓存可以设置不同的过期策略:

  • 第一级缓存通常设置较短的过期时间,以保证数据的新鲜性。
  • 第二级缓存可以设置较长的过期时间,或根据需要不设置过期时间,作为一个稳定的数据备份。

缓存雪崩

缓存雪崩是指缓存中大量数据同时过期,导致所有的请求都直接访问数据库,可能会使数据库压力过大甚至崩溃。

成因

  • 缓存设置了相同的过期时间,导致大量数据同时过期。
  • 缓存服务崩溃,所有数据丢失。

解决方案

  • 不同的过期时间:为缓存数据设置不同的过期时间,避免同时过期。(可以在设置缓存过期时间时加入随机值)
  • 缓存数据预热:系统启动时预先加载热点数据到缓存中。
  • 限流降级:在系统入口处使用限流降级策略,避免在缓存失效时,突发流量直接打到数据库。
  • 使用多级缓存策略
  • 使用高可用的缓存架构:比如使用Redis集群,提高缓存系统的稳定性和容错能力。

总结一下上面的三个常见的生产问题:缓存穿透、缓存击穿、缓存雪崩;

  • 1、缓存穿透指的不存在的数据进行访问缓存,并打到数据库中,解决方案一般有:空对象缓存、布隆过滤器
  • 2、缓存击穿和缓存雪崩有点像,都是缓存失效,但前者一般是热点数据,后者是大量数据同时过期;
    • 热点数据建议做一下预热处理,在快要过期的时候进行处理一下;或者说常用的两种处理方案:
      • 设置热点数据不过期,不过后续需要手动进行更新缓存或者程序更新
      • 设置互斥锁,保证同一时刻访问缓存的数据(缓存中不存在的数据),此时只有一个
    • 大量数据过期建议使用随机过期时间,同时可以设置一下限流,防止大量数据同时请求;也可以设置多级的一个缓存策略操作。

数据一致性 🚩

如何保证缓存和数据库的数据一致性

阅读:

简介相关内容:我们考虑引入 缓存 是打算提高读性能,引入缓存后,之前放入到数据库中的数据,现在需要放到数据中存取,我们需要怎么操作? →

  • 全量数据刷到缓存中(最简单)
    • 数据库的数据,全量刷入缓存(不设置失效时间)
    • 写请求只更新数据库,不更新缓存
    • 启动一个定时任务,定时把数据库的数据,更新到缓存中
  • 缓存中只保留最近访问的「热数据」
    • 写请求依旧只写数据库
    • 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
    • 同时,写入缓存中的数据,都设置失效时间
  • 更新数据库 + 更新缓存
    • 不推荐更新数据库 + 更新缓存的方案
    • 写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列「计算」得出一个值,才把这个值才写到缓存中
  • 旁路缓存模式: 遇到写请求的时候是先更新数据库,再删除 cache

思考:

  • 一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。
  • 既然决定使用缓存,就必须容忍「一致性」问题,我们只能尽可能地去降低问题出现的概率。

缓存和数据库不一致

一般是采用:Cache Aside Pattern(旁路缓存模式): 遇到写请求的时候是先更新数据库,再删除 cache

这种情况下发生数据不一致的情况比较小,

在这种情况下,在数据库更新和缓存删除之间有读取请求,它也只会读到旧的数据,一旦缓存被删除,下一个读取请求将从数据库中获取最新的数据,并更新缓存,从而保持了一致性。

需要避免缓存和数据库不一致需要考虑的点:避免第二步操作【删除 cache】失败

解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本)
    1. 我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。
  2. 增加 cache 更新重试机制(常用)
    1. 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,
    2. 重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

第二种方式采用方案:

  1. 引入消息队列来进行异步重试操作(将删除 cache 操作放在消费者这步操作)
  2. 订阅数据库变更日志,再操作缓存

当缓存操作失败时,通过设置重试机制来确保缓存最终能够与数据库数据保持一致。具体步骤包括:

A. 直接重试

在缓存操作(如删除、更新)失败时,立即进行重试。可以设定一个最大重试次数,避免无限重试。

B. 延时重试

如果直接重试仍然失败,可以将失败的操作延时一段时间后再重试,延时重试可以通过定时任务或延时队列实现。

C. 失败队列

如果重试多次后仍然失败,将更新失败的key存入一个特定的队列中。等到缓存服务恢复正常后,再统一处理这些失败的操作。

具体实施方案

引入消息队列(异步重试)

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

使用消息队列作为异步处理机制,将需要更新或删除的缓存操作作为消息发送到队列中,由消费者负责执行实际的缓存更新任务。这样即使缓存服务暂时不可用,也不会影响主业务流程,待缓存服务恢复后再通过消费消息来同步缓存。

订阅数据库变更日志

另一个高级的方案是直接订阅数据库的变更日志(如果数据库支持),如MySQL的binlog。通过解析变更日志,对相关缓存进行更新或删除操作。这种方法可以确保缓存数据的最终一致性,但实现复杂,对数据库和缓存系统的要求较高。


哪些情况可能导致 Redis 阻塞

10、Redis 集群 🚩💣

看一下这篇文字: https://www.yuque.com/snailclimb/mf2z3k/ks9olb19hc9wse5k

里面讲述了从 主从节点复制 → 哨兵机制 Sentinel → Redis 集群Cluster (自动配置了哨兵模式,且建议至少一主一从节点,三个主节点以上的单数节点集群模式)

主从复制

简单来说,主从复制就是将一台Redis主节点的数据复制到其他的Redis从节点中,尽最大可能保证Redis主节 点和从节点的数据是一致的。

  • 一个Redis服务器充当主节点(master),而一个或多个Redis服务器充当从节点(slave)
  • 从节点的数据会自动与主节点保持同步,这样就能在多个节点间分担读请求的压力,同时也提供了数据的冗余备份。

主从复制是Redis高可用的基石,Redis Sentinel以及Redis Cluster都依赖于主从复制。

主从复制这种方案不仅保障了Redis 服务的高可用,还实现了读写分离,提高了系统的并发量,尤其是读并发 量。

主从复制下从节点会主动删除过期数据吗?

我们知道,Rd引s中常用的过期数据的删除策胳有两个

  • 惰性删除:只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期key没有被删除。
  • 定期删除:每隔一段时间抽取一批key执行删除过期 key 操作。并且,Redis底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Rdis采用的是定期删除+惰性/懒汉式删除。


Redis实现定期删除和惰性删除的机制:

定期删除(Periodic Deletion)

  1. 周期性操作:Redis服务器会周期性地执行定期删除操作。具体地,它默认每100ms随机抽查一些键,并检查这些键是否设置了过期时间。
  2. 随机抽样检查:在每次定期删除操作中,Redis并不是检查所有键,而是从当前数据库的键空间中随机抽取一定数量的键进行检查。这个数量可以通过配置hz参数和active-expire-effort参数来调整。
  3. 删除过期键:对于每个被抽查的键,Redis会检查其是否已经过期。如果已经过期,则将其从数据库中删除。

这种方法可以有效地减少因过期键积累导致的内存浪费,但由于它是基于随机抽样的,不能保证所有过期键都会被及时删除。

惰性删除(Lazy Deletion)

  1. 访问时检查:当客户端尝试访问任何键时,Redis首先会检查该键是否设置了过期时间,并判断该键是否已经过期。
  2. 即时删除:如果该键已经过期,Redis会立即将其删除,然后返回一个错误(例如,当尝试获取一个已经过期的键时,会返回(nil)表示键不存在)。
  3. 无额外开销:对于那些从未被访问的过期键,它们将不会占用额外的CPU资源进行检查。这意味着惰性删除几乎没有额外的性能开销。

主从复制方式下的主从节点的数据同步方案

有三种同步方案

  • Redis 2.8 之前的 SYNC 方案
  • Redis 2.8 PSYNC 方案
  • Redis 4.0 PSYNC 2.0 方案

工作原理:

to be contined....

  1. 同步过程:当从节点启动并连接到主节点时,它会发送一个SYNC命令给主节点。主节点接收到这个命令后,会开始在后台保存快照(执行bgsave),同时收集所有接收到的写命令。快照完成后,主节点将快照文件和这期间收集的所有写命令发送给从节点。从节点首先加载快照文件来构建数据集,然后执行写命令来与主节点的数据状态保持一致。
  2. 命令传播:一旦初始同步完成,主节点会持续将所有接收到的写命令实时发送给从节点。这保证了主从节点间数据的实时一致性。
  3. 断线重连:如果从节点与主节点间的连接断开,当重新连接后,从节点会尝试自动重新同步数据。Redis 2.8版本后,支持部分重同步(PSYNC),这意味着在某些情况下,从节点只需要同步断开期间的数据变更,而不需要重新同步所有数据。

哨兵模式(Sentinel)

集群(Cluster)

11、Reids 使用规范


参考